/**
 *     This file is part of cesm-ws.
 *
 *     cesm-ws, Web Service for Community Earth System Model
 *
 *     Copyright (c) 2010, Purdue University and/or affilicates or thrid-party contributors as
 *     indicated by the @author tags or express copyright attribution statements
 *     applied by the authors. All third-party contributions are distributed under
 *     license by Purdue University.
 *
 *     cesm-ws is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with this distribution.  If not, see <http://www.gnu.org/licenses/>.
 */

package edu.purdue.rcac.cesm.cron;

import static edu.purdue.rcac.cesm.Constants.wrapParam;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.management.MBeanServer;

import org.apache.log4j.Logger;
import org.globus.ftp.exception.FTPException;
import org.globus.gsi.GlobusCredentialException;
import org.ietf.jgss.GSSException;
import org.jboss.ejb3.annotation.Depends;
import org.jboss.ejb3.annotation.ResourceAdapter;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.mx.util.MBeanServerLocator;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.StatefulJob;

import edu.purdue.rcac.cesm.Constants.ParamName;
import edu.purdue.rcac.cesm.persistence.dao.CaseManager;
import edu.purdue.rcac.cesm.persistence.model.CESMCaseConfig;
import edu.purdue.rcac.cesm.persistence.model.CESMCaseRuntime;
import edu.purdue.rcac.cesm.persistence.model.CESMCaseConfig.CaseStage;
import edu.purdue.rcac.cesm.persistence.model.CESMCaseRuntime.JobStatus;
import edu.purdue.rcac.cesm.submitter.CESMGlobusSubmitter;
import edu.purdue.rcac.grid.submit.SubmissionException;
import edu.purdue.rcac.service.dispatch.ServiceDispatcher;
import edu.purdue.rcac.service.dispatch.ServiceDispatcherManagement;

@MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName = "cronTrigger", propertyValue = "0/20 * * * * ?") })
@ResourceAdapter("quartz-ra.rar")
@Depends(CaseManager.OBJECT_NAME)
public class StatusUpdateCronMDBean implements StatefulJob {
	private static long cronId = 0;
	private CaseManager caseManager;
	private ServiceDispatcherManagement dispatcher;
	private Logger logger;
	private final static String TRACK_RESUBMIT_PBSID = "track_resubmit_pbsid";
	private final static String TRACK_FILE = "track_resubmit_pbsid.out";
	private final static String TRACK_ERR_FILE = "track_resubmit_pbsid.err";

	private final static String UPDATE_ACTION = "update_all_runtimes";
	private final static String UPDATE_FILE = "runtime_updates.out";
	private final static String UPDATE_ERR_FILE = "runtime_updates.err";

	private long id = 0;

	public StatusUpdateCronMDBean() {
		logger = Logger.getLogger(StatusUpdateCronMDBean.class.getName());
		getCaseManager();
		getServiceDispatcher();
	}

	public void execute(JobExecutionContext ctx) throws JobExecutionException {
		try {
			// display cron job id
			if (++cronId == 100000000) {
				cronId = 0;
			}
			id = cronId;
			logger = Logger.getLogger(String.format("Cron #%d", id));
			logger.info(String.format(">>>>>>>>>> CESM Update Cron #%d starts",
					id));

			// check for null objects
			if (caseManager == null)
				getCaseManager();

			if (dispatcher == null)
				getServiceDispatcher();

			// load all pbsids that need to be checked
			String pbsids = "";
			trackResubmitPbsid();
			List<CESMCaseRuntime> runtimes = caseManager
					.listAllUnfinishedRuntimes();
			for (CESMCaseRuntime rt : runtimes) {
				if (rt.getCaseConfig().getResubmit() != 0) {
					pbsids += (rt.getPbsid() + "#");
				}
			}
			if (pbsids.length() < 1) {
				logger.info("There is no running job to update");
				updateAllCases();
				return;
			} else {
				pbsids = pbsids.substring(0, pbsids.length() - 1);
				logger.info("Updating jobs with following PBS id:\n\t"
						+ pbsids.replaceAll("#", "\n\t"));
				updateAllRuntimes(pbsids);
				updateAllCases();
			}

		} catch (SubmissionException e) {
			logger.warn("Can not update due to Globus submission exception", e);
		} catch (IOException e) {
			logger.warn("Can not update due to IO exception", e);
		} catch (FTPException e) {
			logger.warn("Can not update due to FTP exception", e);
		} catch (GlobusCredentialException e) {
			logger.error(e);
		} catch (GSSException e) {
			logger.error(e);
		} catch (Exception e) {
			logger.error(e);
		} finally {
			logger.info(String.format(
					"<<<<<<<<<< CESM Update Cron #%d ended.\n\n", id));
		}
	}

	public void trackResubmitPbsid() throws Exception {
		CESMGlobusSubmitter submitter = new CESMGlobusSubmitter();
		Map<String, String> argsMap = new HashMap<String, String>();
		argsMap.put(ParamName.ACTION.name(), TRACK_RESUBMIT_PBSID);
		argsMap.put(ParamName.FILE.name(), TRACK_FILE);

		submitter.setArgsMap(argsMap);
		submitter.submit();
		Pattern pattern = Pattern
				.compile("([a-zA-Z0-9_]+) ([^ ]+) ([^ ]+) (\\d+)\\n");

		String result = submitter.readAsString();
		logger.info("RESULT: " + result);
		Matcher matcher = pattern.matcher(result);

		while (matcher.find()) {
			String userName = matcher.group(1).trim();
			String caseName = matcher.group(2).trim();
			String pbsid = matcher.group(3).trim();
			int resubmit = Integer.valueOf(matcher.group(4));

			if (caseManager.findByPbsid(pbsid) == null) {
				CESMCaseConfig caseConfig = caseManager.findByOwnerAndName(
						userName, caseName);
				CESMCaseRuntime rt = new CESMCaseRuntime(pbsid, caseConfig);

				for (CESMCaseRuntime runtime : caseManager
						.findByCaseConfigId(caseConfig.getId())) {
					if (runtime.getQueue() != null
							&& !"".equals(runtime.getQueue())) {
						rt.setQueue(runtime.getQueue());
					}
				}
				caseManager.save(rt);
			}
		}
	}

	public void updateAllRuntimes(String pbsids) throws Exception {
		CESMGlobusSubmitter submitter = new CESMGlobusSubmitter();
		Map<String, String> argsMap = new HashMap<String, String>();
		argsMap.put(ParamName.ACTION.name(), UPDATE_ACTION);
		argsMap.put(wrapParam("pbsids"), pbsids);
		argsMap.put(ParamName.FILE.name(), UPDATE_FILE);

		submitter.setArgsMap(argsMap);
		submitter.submit();
		String result = submitter.readAsString();

		logger.info("Got result: \n\t" + result.replace("\n", "\n\t"));
		try {
			parseResultAndWriteBack(result);
		} catch (Exception e) {
			argsMap.put(ParamName.ACTION.name(), "get_file_content");
			argsMap.put(ParamName.FILE.name(), UPDATE_ERR_FILE);
			submitter.setArgsMap(argsMap);
			submitter.submit();
			throw new Exception(submitter.readAsString());
		}
	}

	public void updateAllCases() throws Exception {
		for (CESMCaseConfig caseConfig : caseManager.listAllRunningCases()) {
			List<CESMCaseRuntime> runtimes = caseConfig.getRuntimes();
			CESMGlobusSubmitter submitter = new CESMGlobusSubmitter();
			Map<String, String> argsMap = new HashMap<String, String>();
			argsMap.put(ParamName.ACTION.name(), "get_CaseStatus");
			argsMap.put(ParamName.USERNAME.name(), caseConfig.getOwner());
			argsMap.put(ParamName.CASENAME.name(), caseConfig.getName());
			boolean terminated = true;
			logger.info("UPDATING: " + caseConfig.getName());
			for (CESMCaseRuntime rt : runtimes) {
				if (!rt.getJobStatus().equals(JobStatus.DONE)
						&& !rt.getJobStatus().equals(
								JobStatus.COMPLETED_OR_DELETED)
						&& !rt.getJobStatus().equals(JobStatus.CANCELLED)) {
					terminated = false;
					break;
				}
			}
			if (terminated) {
				caseConfig.setStage(CaseStage.DONE);
				submitter.setArgsMap(argsMap);
				String result = submitter.readAsString();
				System.out.println(result);
				String[] lines = result.split("\n");
				String lastLine = lines[lines.length - 1];
				if (lastLine.contains("SUCCESSFUL"))
					caseConfig.setStage(CaseStage.SUCCESS);
				else if (lastLine.contains("FAILED"))
					caseConfig.setStage(CaseStage.FAILED);
				else
					caseConfig.setStage(CaseStage.RUNNING);
				logger.info("STAGE CHANGE TO: "+caseConfig.getStage());
			}
		}
	}

	private void getCaseManager() {
		MBeanServer server = MBeanServerLocator.locate();
		try {
			logger.info("Look up object name: " + CaseManager.OBJECT_NAME);
			caseManager = (CaseManager) MBeanProxyExt.create(CaseManager.class,
					CaseManager.OBJECT_NAME, server);
		} catch (Exception e) {
			logger.error(
					"can not start up CESMUpdateCronMDBean because of dependency '"
							+ CaseManager.OBJECT_NAME + "' was not found", e);
		}
	}

	private void getServiceDispatcher() {
		MBeanServer server = MBeanServerLocator.locate();
		try {
			logger.info("Look up object name: " + ServiceDispatcher.OBJECT_NAME);
			dispatcher = (ServiceDispatcherManagement) MBeanProxyExt.create(
					ServiceDispatcherManagement.class,
					ServiceDispatcher.OBJECT_NAME, server);
		} catch (Exception e) {
			logger.error(
					"can not start up CESMUpdateCronMDBean because of dependency '"
							+ ServiceDispatcher.OBJECT_NAME + "' was not found",
					e);
		}
	}

	private void parseResultAndWriteBack(String result) throws Exception {
		for (String line : result.split("\n")) {
			String items[] = line.split(",");
			if (items.length < 4) {
				logger.debug("met incomplete line: " + line);
				continue;
			}
			String pbsid = items[0].trim();
			try {
				String usedWallTime = items[1].trim();
				String usedCPUTime = items[2].trim();
				char jobStatusChar = items[3].trim().charAt(0);
				JobStatus jobStatus = CESMCaseRuntime.JOB_STATUS_MAP
						.get(jobStatusChar);
				CESMCaseRuntime caseRuntime = caseManager.findByPbsid(pbsid);
				if (caseRuntime != null
						&& (!usedWallTime.equals(caseRuntime.getUsedWallTime())
								|| !usedCPUTime.equals(caseRuntime
										.getUsedCPUTime()) || jobStatus != caseRuntime
								.getJobStatus())) {
					caseRuntime.setUsedCPUTime(usedCPUTime);
					caseRuntime.setUsedWallTime(usedWallTime);
					caseRuntime.setJobStatus(jobStatus);
					caseManager.merge(caseRuntime);
				}
			} catch (Exception e) {
				throw e;
			}
		}
	}

}
